from flask import render_template, flash, redirect
from flask.globals import request
from flask.helpers import url_for
from flask_login.utils import login_required
from flask_migrate import current
from app import app, db
from app.forms import LoginForm, RegistrationForm, EditProfileForm, PostForm
from flask_login import current_user, login_user, logout_user
from app.models import User, Post
from werkzeug.urls import url_parse
from datetime import datetime
from app.forms import ResetPasswordRequestForm
from app.email import send_password_reset_email
from app.forms import ResetPasswordForm


# 装饰器的常见模式是使用它们将函数注册为某些事件的回调函数
# ＠app.route修饰器在作为参数给出的URL和函数之间创建一个关联
# 在这个例子中，有两个装饰器，它们将URL /和/index索引关联到这个函数

# Flask-Login使用名为@login_required的装饰器来拒绝匿名用户的访问以保护某个视图函数
@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
@login_required
def index():
    form = PostForm()
    if form.validate_on_submit():
        post = Post(body=form.post.data, author=current_user)
        db.session.add(post)
        db.session.commit()
        flash('Your post is now live!')
        #  通过重定向来响应Web表单提交产生的POST请求是一种标准做法
        # 这有助于缓解在Web浏览器中执行刷新命令的烦恼
        # 当你点击刷新键时，所有的网页浏览器都会重新发出最后的请求
        #  如果带有表单提交的POST请求返回一个常规的响应，那么刷新将重新提交表单

        # 这个简单的技巧叫做Post/Redirect/Get模式。 
        # 它避免了用户在提交网页表单后无意中刷新页面时插入重复的动态。
        return redirect(url_for('index'))
    # posts = [
    #     {
    #         'author': {'username': 'john'},
    #         'body': 'beautiful day in Portland!'
    #     },
    #     {
    #         'author': {'username': 'Susan'},
    #         'body': 'The Avengers movie was so cool!'
    #     },
    # ]

    # User类的followed_posts方法返回一个SQLAlchemy查询对象，
    # 该对象被配置为从数据库中获取用户感兴趣的用户动态。 
    # 在这个查询中调用all()会触发它的执行，返回值是包含所有结果的列表。
    # posts = current_user.followed_posts().all()

    # 分页
    # Flask-SQLAlchemy的所有查询对象都支持paginate方法，需要输入三个参数来调用它：
        # 从1开始的页码
        # 每页的数据量
        # 错误处理布尔标记，
            # 如果是True，当请求范围超出已知范围时自动引发404错误。
            # 如果是False，则会返回一个空列表。
    # paginate方法返回一个Pagination的实例。其items属性是请求内容的数据列表
    page = request.args.get('page', 1, type=int)
    posts = current_user.followed_posts().paginate(
        page, app.config['POSTS_PER_PAGE'], False
    )
    #  如果当前页面位于用户动态集合的末尾或者开头，
    # 那么Pagination实例的has_next或has_prev属性将为'False'
    # url_for()函数的一个有趣的地方是，你可以添加任何关键字参数，
        # 如果这些参数的名字没有直接在URL中匹配使用，
        # 那么Flask将它们设置为URL的查询字符串参数。
    next_url = url_for('index', page=posts.next_num)\
        if posts.has_next else None
    prev_url = url_for('index', page=posts.prev_num)\
        if posts.has_prev else None
    # render_template() 需要传入模板文件名和模板参数的变量列表
    # return render_template('index.html', 
    #                         title='Home Page',
    #                         form=form,
    #                         posts=posts,)
    # return render_template('index.html', title='Home Page', form=form,
    #                        posts=posts.items)
    return render_template('index.html', title='Home Page', form=form,
                           posts=posts.items, next_url=next_url,
                           prev_url=prev_url)



# 如何将页码并入到应用URL中。 
# 一个相当常见的方法是使用查询字符串参数来指定一个可选的页码，
# 如果没有给出则默认为页面1
    # 第1页，隐含：http://localhost:5000/index
    # 第1页，显式：http://localhost:5000/index?page=1
    # 第3页：http://localhost:5000/index?page=3


# 更容易地发现和关注用户
@app.route('/explore')
@login_required
def explore():
    # 展示所有用户的全部动态
    # posts = Post.query.order_by(Post.timestamp.desc()).all()

    # 分页
    page = request.args.get('page', 1, type=int)
    posts = Post.query.order_by(Post.timestamp.desc()).paginate(
        page, app.config['POSTS_PER_PAGE'], False)
    next_url = url_for('explore', page=posts.next_num) \
        if posts.has_next else None
    prev_url = url_for('explore', page=posts.prev_num) \
        if posts.has_prev else None
    # render_template()引用了我在应用的主页面中使用的index.html模板
    # 这个页面与主页非常相似，所以我决定重用这个模板。 
    # 但与主页不同的是，在发现页面不需要一个发表用户动态表单，
    # 所以在这个视图函数中，我没有在模板调用中包含form参数。
    # return render_template('index.html', title='Explore', posts=posts)
    # return render_template("index.html", title='Explore', posts=posts.items)
    return render_template("index.html", title='Explore', posts=posts.items,
                        next_url=next_url, prev_url=prev_url)






# 视图函数接受GET和POST请求，并覆盖了默认的GET
# HTTP协议规定对GET请求需要返回信息给客户端
# 当浏览器向服务器提交表单数据时，通常会使用POST请求
# （实际上用GET请求也可以，但这不是推荐的做法）
@app.route('/login', methods=['GET', 'POST'])
def login():
    # current_user变量来自Flask-Login，可以在处理过程中的任何时候调用以获取用户对象。 
    # 这个变量的值可以是数据库中的一个用户对象（Flask-Login通过我上面提供的用户加载函数回调读取），
    # 或者如果用户还没有登录，则是一个特殊的匿名用户对象
    if current_user.is_authenticated:
        return redirect(url_for('index'))

    form = LoginForm()
    # 实例方法执行form校验的工作。当浏览器发起GET请求的时候，它返回False
    # 当用户在浏览器点击提交按钮后，浏览器会发送POST请求
    # form.validate_on_submit()就会获取到所有的数据，运行字段各自的验证器，全部通过之后就会返回True，这表示数据有效。
    # 不过，一旦有任意一个字段未通过验证，这个实例方法就会返回False
    # 查询中调用all()方法时， 将执行该查询并获得与该查询匹配的所有结果的列表。 
    # 当你只需要一个结果时，通常使用first()方法。
    if form.validate_on_submit():
        # SQLAlchemy查询对象的filter_by()方法的结果是一个只包含具有匹配用户名的对象的查询结果集。 
        # 因为我知道查询用户的结果只可能是有或者没有，所以我通过调用first()来完成查询，
        # 如果存在则返回用户对象;如果不存在则返回None
        user =  User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        # Flask-Login的login_user()函数。 该函数会将用户登录状态注册为已登录，
        # 这意味着用户导航到任何未来的页面时，应用都会将用户实例赋值给current_user变量
        login_user(user, remember=form.remember_me.data)
        # 在用户通过调用Flask-Login的login_user()函数登录后，
        # 应用获取了next查询字符串参数的值
        # Flask提供一个request变量，其中包含客户端随请求发送的所有信息
        # 实际上有三种可能的情况需要考虑，以确定成功登录后重定向的位置：
            # 如果登录URL中不含next参数，那么将会重定向到本应用的主页。
            # 如果登录URL中包含next参数，其值是一个相对路径（换句话说，该URL不含域名信息），那么将会重定向到本应用的这个相对路径。
            # 如果登录URL中包含next参数，其值是一个包含域名的完整URL，那么重定向到本应用的主页。
        # 第三种情况是为了使应用更安全。 攻击者可以在next参数中插入一个指向恶意站点的URL，
        # 因此应用仅在重定向URL是相对路径时才执行重定向，这可确保重定向与应用保持在同一站点中。 
        # 为了确定URL是相对的还是绝对的，我使用Werkzeug的url_parse()函数解析，然后检查netloc属性是否被设置。
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('index')
        # flash('Login requested for user {}, remember_me={}'.format(
        #     form.username.data, form.remember_me.data))
        return redirect(next_page)
    # form=form的语法: 
    # 左边的form代表在模板中引用的变量名称，
    # 右边则是传入的form实例
    return render_template('login.html', title='Sign In', form=form)



@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index'))


@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('Congratulations, you are now a registered user!')
        return redirect(url_for('login'))
    return render_template('register.html', title='Register', form=form)

# 被<和>包裹的URL <username>是动态的
# 当一个路由包含动态组件时，Flask将接受该部分URL中的任何文本，
# 并将以实际文本作为参数调用该视图函数。
# 如果客户端浏览器请求URL /user/susan，
# 则视图函数将被调用，其参数username被设置为'susan'
# http://127.0.0.1:5000/user/dillon
# http://127.0.0.1:5000/user/susan
@app.route('/user/<username>')
@login_required
def user(username):
    # first_or_404()，当有结果时它的工作方式与first()完全相同，
    # 但是在没有结果的情况下会自动发送404 error给客户端
    # 以这种方式执行查询，我省去检查用户是否返回的步骤，
    # 因为当用户名不存在于数据库中时，函数将不会返回，而是会引发404异常。
    user = User.query.filter_by(username=username).first_or_404()
    # posts = [
    #     {'author': user, 'body': 'Test post #1'},
    #     {'author': user, 'body': 'Test post #2'}
    # ]
    # 分页
    page = request.args.get('page', 1, type=int)
    posts = user.posts.order_by(Post.timestamp.desc()).paginate(
        page, app.config['POSTS_PER_PAGE'], False)
    # 请注意，由url_for()函数生成的分页链接需要额外的username参数，
    # 因为它们指向个人主页，个人主页依赖用户名作为URL的动态组件。
    next_url = url_for('user', username=user.username, page=posts.next_num) \
        if posts.has_next else None
    prev_url = url_for('user', username=user.username, page=posts.prev_num) \
        if posts.has_prev else None
    # return render_template('user.html', user=user, posts=posts)
    return render_template('user.html', user=user, posts=posts.items,
                        next_url=next_url, prev_url=prev_url)

# Flask中的@before_request装饰器注册在视图函数之前执行的函数
# 现在我可以在一处地方编写代码，并让它在任何视图函数之前被执行
# 该代码简单地实现了检查current_user是否已经登录，
# 并在已登录的情况下将last_seen字段设置为当前时间
@app.before_request
def before_request():
    if current_user.is_authenticated:
        # 应用应该以一致的时间单位工作，标准做法是使用UTC时区
        current_user.last_seen = datetime.utcnow()
        # 什么在提交之前没有db.session.add()
        # 考虑在引用current_user时，Flask-Login将调用用户加载函数，
        # 该函数将运行一个数据库查询并将目标用户添加到数据库会话中
        # 所以你可以在这个函数中再次添加用户，但是这不是必须的，因为它已经在那里了
        db.session.commit()


@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
    # 大多数情况下，以后在编辑个人资料时出现用户名重复的提交将被友好地阻止
    # 但这不是一个完美的解决方案，因为当两个或更多进程同时访问数据库时，这可能不起作用
    # 除了有很多服务器进程并且非常繁忙的应用之外，这种情况是不太可能的，所以现在我不会为此担心。
    form = EditProfileForm(current_user.username)
    if form.validate_on_submit():
        current_user.username = form.username.data
        current_user.about_me = form.about_me.data
        db.session.commit()
        flash('Your changes have been saved.')
        return redirect(url_for('edit_profile'))
    elif request.method == 'GET':
        form.username.data = current_user.username
        form.about_me.data = current_user.about_me
    return render_template('edit_profile.html', title='Edit Profile',
                            form=form)

@app.route('/follow/<username>')
@login_required
def follow(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash('User {} not found.'.format(username))
        return redirect(url_for('index'))
    if user == current_user:
        flash('You cannot follow yourself!')
        return redirect(url_for('user', username=username))
    current_user.follow(user)
    db.session.commit()
    flash('You are following {}!'.format(username))
    return redirect(url_for('user', username=username))

@app.route('/unfollow/<username>')
@login_required
def unfollow(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash('User {} not found.'.format(username))
        return redirect(url_for('index'))
    if user == current_user:
        flash('You cannot unfollow yourself!')
        return redirect(url_for('user', username=username))
    current_user.unfollow(user)
    db.session.commit()
    flash('You are not following {}.'.format(username))
    return redirect(url_for('user', username=username))



@app.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = ResetPasswordRequestForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user:
            send_password_reset_email(user)
        flash('Check your email for the instructions to reset your password')
        return redirect(url_for('login'))
    return render_template('reset_password_request.html',
                           title='Reset Password', form=form)


# 在这个视图函数中，我首先确保用户没有登录，然后通过调用User类的令牌验证方法来确定用户是谁。 
# 如果令牌有效，则此方法返回用户；如果不是，则返回None，并将重定向到主页。
@app.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    user = User.verify_reset_password_token(token)
    if not user:
        return redirect(url_for('index'))
    # 如果令牌是有效的，那么我向用户呈现第二个表单，需要用户其中输入新密码
    form = ResetPasswordForm()
    if form.validate_on_submit():
        user.set_password(form.password.data)
        db.session.commit()
        flash('Your password has been reset.')
        return redirect(url_for('login'))
    return render_template('reset_password.html', form=form)